今天要來學習的 Reducer 的應用,主要內容為 Reducer 的操作步驟。
隨著程式越來越複雜,我們可能會遇到下面這樣的情況
像是在這裡,需要處理三種事件:「追加項目」、「刪除項目」、「編輯」。
import { useState } from "react";
import AddTask from "./AddTask.js";
import TaskList from "./TaskList.js";
export default function TaskApp() {
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: "Visit Kafka Museum", done: true },
{ id: 1, text: "Watch a puppet show", done: false },
{ id: 2, text: "Lennon Wall pic", done: false },
];
若想要統一管理這樣的 event handlers,可以使用 Reducer 來操作,一共用三個步驟。
這邊要做的事情是改變 event handlers 的內容,先將 state 相關的邏輯移除(這個後續會再處理)
,留下 event handlers 更純粹要操作事情,也就是告訴 React 使用者正在操作什麼事情,例如「新增了一個任務」、「更改了一個任務」或「刪除了一個任務」。實際來看看程式碼:
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleAddTask(text) {
dispatch({
type: "added",
id: nextId++,
text: text,
});
}
我們使用了 dispatch 去做這件事,可以看見 dispatch 內部傳入了一個 object,這個 object 稱為「action object」。
「action object」的內部通常可以在type
以 string 來說明發生什麼事,其他可以按需求傳入額外的資訊即可。像是這樣子:
dispatch({
// 描述發生什麼事情
type: "what_happened",
// 其他額外資訊
});
在 Reducer function 我們要使用剛剛的 action object 和先前拿掉的 state 的邏輯。
function yourReducer(state, action) {
// return next state for React to set
}
Reducer function 會有二個參數,一個是 state 的邏輯,另一個是 action object。先讓我們使用 if/else statement 來寫寫看:
function tasksReducer(tasks, action) {
if (action.type === "added") {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === "changed") {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === "deleted") {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error("Unknown action: " + action.type);
}
}
像以上這樣透過判定 action object 的 type (也就是第一步的要操作什麼事情)來決定回傳的東西。要留意這邊回傳的會是「下一個 state」(next state)。另外,這邊雖然是使用 if/else statement 來操作,在通常的情況下多為使用 switch statement。這樣子就完成第二步的操作了。
最後要來把剛剛學的結合起來。
首先需要先 import useReducer
import { useReducer } from "react";
再來,將useState
取代為useReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
這邊需要留意的是和僅需要初始值的useState
不同useReducer
需要「reducer function」和「初始的 state(initial state)」二個 arguments。而它 return 一個「狀態值(stateful value)」和「dispatch function」。
回顧一下,狀態值(stateful value)是什麼?
狀態值是表示應用程式當前狀態的一個值。它代表了應用程式的數據或狀態的快照。而在目前的例子 return 的 dispatch 用於觸發列表的更新,並會在 event handlers 當中被呼叫。
將全部結合起來就會是以下的程式碼:
import { useReducer } from "react";
import AddTask from "./AddTask.js";
import TaskList from "./TaskList.js";
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function handleAddTask(text) {
dispatch({
type: "added",
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: "changed",
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: "deleted",
id: taskId,
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case "added": {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case "changed": {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case "deleted": {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error("Unknown action: " + action.type);
}
}
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: "Visit Kafka Museum", done: true },
{ id: 1, text: "Watch a puppet show", done: false },
{ id: 2, text: "Lennon Wall pic", done: false },
];
讓我們比較看看useState
和useReducer
:
方面 | useState | useReducer |
---|---|---|
程式碼大小 | 需要較少的程式碼 | 需要定義 reducer 函數和派發 actions |
可讀性 | 當狀態更新簡單時容易閱讀 | 當狀態更新複雜時,更容易分離更新邏輯 |
調試 | 難以找到狀態設置錯誤的位置和原因 | 可以在 reducer 中添加 console log 以更清晰地追蹤狀態更新和原因 |
測試 | 需要在元件內測試 | 可以將 reducer 單獨測試 |
至於要選用 useState 還是 useReducer,這個可以依據專案的需求或是偏好而定。